package br.uniceub.fsw.arquitetura.persistencia.conexao;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import br.uniceub.fsw.arquitetura.persistencia.exception.BancoDadosException;

/**
 * Implementa um simples pool de conexões com uma base de dados.
 * 
 * TODO melhorar parâmetros de conexão (properties?).
 * 
 * @author Daniel Rochetti
 */
public final class PoolConexoes {

	private static final String DRIVER = "com.mysql.jdbc.Driver";
	private static final String URL = "jdbc:mysql://localhost:3306/fswuniceub?autoReconnect=true&useUnicode=true&characterEncoding=utf8";
	private static final String USER = "fswuniceub";
	private static final String PASSWORD = "fswuniceub";
	
	private int max = 30;
	private List<Connection> conexoesLivres;
	private List<Connection> conexoesUsadas;
	
	private static PoolConexoes instancia;
	
	private PoolConexoes() {
		carregarDriver();
		conexoesLivres = new ArrayList<Connection>(max);
		conexoesUsadas = new ArrayList<Connection>(max);
	}
	
	public static PoolConexoes getInstancia() {
		if (instancia == null) {
			instancia = new PoolConexoes();
		}
		return instancia;
	}

	/**
	 * Carrega o Driver JDBC informado.
	 */
	private void carregarDriver() {
		try {
			Class<?> driverClass = Class.forName(DRIVER);
			Driver driver = (Driver) driverClass.newInstance();
			DriverManager.registerDriver(driver);
		} catch (ClassNotFoundException e) {
			throw new BancoDadosException("O Driver do Banco de Dados não " +
					"pode ser carregado! Verifique suas configurações", e);
		} catch (InstantiationException e) {
			throw new BancoDadosException("Impossível instanciar o driver!", e);
		} catch (IllegalAccessException e) {
			throw new BancoDadosException("Não foi possível acessar o driver.", e);
		} catch (SQLException e) {
			throw new BancoDadosException(e.getMessage(), e);
		}
	}

	/**
	 * Cria uma nova conexão, caso seja possível.
	 * @return Connection conexao.
	 */
	private Connection criarConexao() {
		int todas = conexoesLivres.size() + conexoesUsadas.size();
		if (todas < 30) {
			try {
				return DriverManager.getConnection(URL, USER, PASSWORD);
			} catch (SQLException e) {
				throw new BancoDadosException(e.getMessage(), e);
			}
		} else {
			throw new BancoDadosException("Número máximo de conexões " +
					"atingidas no pool!");
		}
	}

	/**
	 * Obtém a última conexão do pool.
	 * 
	 * TODO rever: qual melhor conexão para retirar do pool?
	 * @return Connection conexao.
	 */
	private Connection getUltimaNoPool() {
		int index = conexoesLivres.size() - 1;
		return (Connection) conexoesLivres.get(index);
	}

	/**
	 * Destrói a conexão informada.
	 * @param conn conexão.
	 */
	private void destruirConexao(Connection conn) {
		if (conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				throw new RuntimeException("Erro ao destruir conexão!");
			} finally {
				conn = null;
			}
		}
	}

	/**
	 * Obtém uma conexão do pool (recursivo, tenta pegar uma conexão
	 * até conseguir uma válida do pool).
	 * @return Connection - conexão válida e livre do pool.
	 */
	public synchronized Connection getConexao() {
		Connection conn = null;
		if (!conexoesLivres.isEmpty()) {
			conn = getUltimaNoPool();
			if (!validarConexao(conn)) {
				conexoesLivres.remove(conn);
				destruirConexao(conn);
				conn = getConexao();
			}
		} else {
			conn = criarConexao();
		}
		conexoesLivres.remove(conn);
		conexoesUsadas.add(conn);
		if (conn == null) {
			throw new BancoDadosException("Não foi possível obter " +
					"conexão do pool!");
		}
		return conn;
	}

	/**
	 * Verifica se a conexão ainda é válida.
	 * 
	 * @param conn conexão.
	 * @return {@code true} caso seja ou {@code false} caso contrário.
	 */
	private boolean validarConexao(Connection conn) {
		
		boolean valida = true;
		try {
			valida = !conn.isClosed();
			if (valida) {
				conn.getMetaData();
			}
		} catch (Exception e) {
			valida = false;
		}
		return valida;
	}

	/**
	 * Libera uma conexão para ser utilizada novamente (devolve ao pool).
	 * @param conn conexão
	 */
	public void liberarConexao(Connection conn) {
		if (conn == null) {
			throw new IllegalArgumentException("conexão não pode ser nula!");
		}
		conexoesUsadas.remove(conn);
		conexoesLivres.add(conn);
	}

	/**
	 * Limpa o pool de conexões.
	 */
	public void liberarPool() {
		synchronized (conexoesLivres) {
			for (Connection conn : conexoesLivres) {
				destruirConexao(conn);
			}
		}
	}
}
